Основная задача - построить сервис, который может прогнозировать ЗП, основываясь на данных по профессии, региону размещения, графику работы и другим дотсупным данным.
Требования к модели: модель должна быть интерпетируемой, т.е. мы дожны уметь показать Пользователю основные факторы и их вес в формировании зарплаты. Второе требование - метрика mape <= 0.2.
Основные этапы работы:
EDA. Нужно понять с какими данными работаем, какие из фичей влияют на зарплату. На этом этапе важно понять, правильно ли мы собрали данные? Не ли перекосов в распределениях по локациям? Например у нас 15к вакансий из Мск, 5к вакансий из Питера, а остальные - все остальные регионы РФ. Или у нас в выборке большой перекос в сторону популярыных профессий: кассиров и водителей, а инженеров очень мало.
Обработка данных. Нужно избавиться от выбросов, написать пайплайны для процессинга данных. Сформировать трейн и тест выборки.
Моделирование. Эксперементируем с моделями, для того чтобы выбить на тесте mape <= 0.2, не забывая что модель должна быть интерпретируемой.
Валидация. Проверяем адекватность работы модели.
from google.colab import drive
drive.mount('/content/drive')
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).
%%capture
!pip install https://github.com/pandas-profiling/pandas-profiling/archive/master.zip
#重新启动内核
import os
os._exit(00)
import os
import numpy as np
import pandas as pd
import seaborn as sns
import missingno as msno
import matplotlib.pyplot as plt
#from pandas_profiling import ProfileReport
from ydata_profiling import ProfileReport
df = pd.read_csv('/content/drive/MyDrive/研一_项目研讨_预测工资/data_vacancies.csv')
df.head()
| id | custom_position | schedule | salary_from | salary_to | salary_pay_type | offer_education_id | education_name | education_is_base | education_order_num | city_id | list_regions | work_skills | tags_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 48202096 | Сварщик-сборщик | полный рабочий день | 60000 | 120000 | NaN | 0 | любое | True | 0 | 2 | [4] | ['сварочные работы', 'сборка изделий по чертеж... | NaN |
| 1 | 48202097 | Сварщик-монтажник | полный рабочий день | 60000 | 120000 | NaN | 0 | любое | True | 0 | 2 | [4] | ['монтажные работы', 'строительные работы', 'э... | NaN |
| 2 | 48202098 | Слесарь-сборщик | полный рабочий день | 60000 | 80000 | NaN | 0 | любое | True | 0 | 2 | [4] | ['работа на фрезерных станках', 'слесарный рем... | NaN |
| 3 | 48202356 | Грузчик-упаковщик | частичная занятость | 30000 | 35000 | NaN | 0 | любое | True | 0 | 1 | [3] | ['комплектация товара', 'маркировка', 'стрессо... | [6, 9] |
| 4 | 48202357 | Грузчик-упаковщик | частичная занятость | 30000 | 35000 | NaN | 0 | любое | True | 0 | 57 | [181, 182, 183, 185, 186, 187, 188, 189, 190, ... | ['маркировка', 'стрессоустойчивость', 'погрузо... | [6, 9] |
df.shape
(19489, 14)
df.dtypes
id int64 custom_position object schedule object salary_from int64 salary_to int64 salary_pay_type object offer_education_id int64 education_name object education_is_base bool education_order_num int64 city_id int64 list_regions object work_skills object tags_id object dtype: object
df.isnull().sum()
id 0 custom_position 0 schedule 0 salary_from 0 salary_to 0 salary_pay_type 19383 offer_education_id 0 education_name 0 education_is_base 0 education_order_num 0 city_id 0 list_regions 0 work_skills 0 tags_id 5999 dtype: int64
msno.matrix(df,figsize=(15,8));
msno.bar(df, figsize=(15,8), color="pink");
df['salary_pay_type'].value_counts()
net 74 gross 32 Name: salary_pay_type, dtype: int64
df['tags_id'].value_counts()
[9] 3860 [5, 6, 9] 1537 [6, 9] 1321 [5, 6, 8, 9] 1114 [5, 9] 1071 [5, 6, 8] 923 [6] 877 [5] 692 [5, 6] 549 [6, 8] 358 [6, 8, 9] 323 [7, 9] 166 [8] 125 [5, 8, 9] 108 [7] 96 [8, 9] 82 [5, 6, 7, 9] 71 [5, 8] 62 [7, 8] 45 [5, 7] 38 [6, 7, 9] 21 [5, 7, 9] 17 [6, 7] 11 [5, 6, 7] 9 [6, 7, 8] 7 [5, 6, 7, 8, 9] 2 [6, 7, 8, 9] 2 [7, 8, 9] 1 [5, 7, 8] 1 [5, 6, 7, 8] 1 Name: tags_id, dtype: int64
df['education_is_base'].value_counts()
True 19489 Name: education_is_base, dtype: int64
Удаление колонки education_is_base
df_2 = df.drop(labels=['education_is_base'],axis=1)
df_2.head()
| id | custom_position | schedule | salary_from | salary_to | salary_pay_type | offer_education_id | education_name | education_order_num | city_id | list_regions | work_skills | tags_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 48202096 | Сварщик-сборщик | полный рабочий день | 60000 | 120000 | NaN | 0 | любое | 0 | 2 | [4] | ['сварочные работы', 'сборка изделий по чертеж... | NaN |
| 1 | 48202097 | Сварщик-монтажник | полный рабочий день | 60000 | 120000 | NaN | 0 | любое | 0 | 2 | [4] | ['монтажные работы', 'строительные работы', 'э... | NaN |
| 2 | 48202098 | Слесарь-сборщик | полный рабочий день | 60000 | 80000 | NaN | 0 | любое | 0 | 2 | [4] | ['работа на фрезерных станках', 'слесарный рем... | NaN |
| 3 | 48202356 | Грузчик-упаковщик | частичная занятость | 30000 | 35000 | NaN | 0 | любое | 0 | 1 | [3] | ['комплектация товара', 'маркировка', 'стрессо... | [6, 9] |
| 4 | 48202357 | Грузчик-упаковщик | частичная занятость | 30000 | 35000 | NaN | 0 | любое | 0 | 57 | [181, 182, 183, 185, 186, 187, 188, 189, 190, ... | ['маркировка', 'стрессоустойчивость', 'погрузо... | [6, 9] |
#int_education_is_base = df['education_is_base'].astype(int)
#int_education_is_base.dtypes
#df['education_is_base'] = int_education_is_base
dtype('int64')
corr_matrix=df_2.corr()
corr_matrix.head()
<ipython-input-79-a9f33b7efaec>:1: FutureWarning: The default value of numeric_only in DataFrame.corr is deprecated. In a future version, it will default to False. Select only valid columns or specify the value of numeric_only to silence this warning. corr_matrix=df_2.corr()
| id | salary_from | salary_to | offer_education_id | education_order_num | city_id | |
|---|---|---|---|---|---|---|
| id | 1.000 | 0.045 | 0.028 | -0.006 | -0.002 | 0.021 |
| salary_from | 0.045 | 1.000 | 0.761 | 0.119 | 0.090 | -0.002 |
| salary_to | 0.028 | 0.761 | 1.000 | 0.086 | 0.049 | -0.017 |
| offer_education_id | -0.006 | 0.119 | 0.086 | 1.000 | 0.691 | -0.023 |
| education_order_num | -0.002 | 0.090 | 0.049 | 0.691 | 1.000 | -0.004 |
plt.subplots(figsize=(15,8))
fig=sns.heatmap(corr_matrix, annot=True, square=True, cmap='RdBu', fmt='.4g') #annot为热力图上显示数据, fmt='.2g'为数据保留两位有效数字, square呈现正方形, vmax最大值为1
fig;
print(f"Всего вакансий в этих данных: {len(df_2['custom_position']):,}")
Всего вакансий в этих данных: 19,489
df_2['education_name'].unique()
array(['любое', 'среднее', 'высшее', 'среднее профессиональное',
'неполное высшее'], dtype=object)
df_2['schedule'].unique()
array(['полный рабочий день', 'частичная занятость', 'удаленная работа',
'сменный график', 'свободный график', 'вахта'], dtype=object)
df_2['salary_to'].describe()
count 19489.000 mean 88490.884 std 55438.161 min 21000.000 25% 51000.000 50% 73000.000 75% 107000.000 max 1200000.000 Name: salary_to, dtype: float64
df_2['salary_from'].describe()
count 19489.000 mean 58869.139 std 30248.195 min 20500.000 25% 40000.000 50% 50000.000 75% 70000.000 max 750000.000 Name: salary_from, dtype: float64
*Что предположим, если тип зарплаты (salary_pay_type) не указан? Поставить "gros"? Зарплату за тип: "gros", "net".*
pd.set_option('display.float_format', lambda x:'%.3f'%x)
df_2.describe()
| id | salary_from | salary_to | offer_education_id | education_order_num | city_id | |
|---|---|---|---|---|---|---|
| count | 19489.000 | 19489.000 | 19489.000 | 19489.000 | 19489.000 | 19489.000 |
| mean | 48505170.910 | 58869.139 | 88490.884 | 0.351 | 2.474 | 22.559 |
| std | 164298.572 | 30248.195 | 55438.161 | 0.970 | 6.482 | 38.694 |
| min | 48202096.000 | 20500.000 | 21000.000 | 0.000 | 0.000 | 1.000 |
| 25% | 48353880.000 | 40000.000 | 51000.000 | 0.000 | 0.000 | 1.000 |
| 50% | 48513907.000 | 50000.000 | 73000.000 | 0.000 | 0.000 | 2.000 |
| 75% | 48668409.000 | 70000.000 | 107000.000 | 0.000 | 0.000 | 57.000 |
| max | 48737872.000 | 750000.000 | 1200000.000 | 4.000 | 25.000 | 272.000 |
education_map = {
'любое': 1,
'среднее': 2,
'высшее': 3,
'среднее профессиональное': 4,
'неполное высшее': 5
}
eda_data = df_2
eda_data['education_name'] = eda_data['education_name'].replace(education_map)
eda_data['graphic'] = eda_data['schedule'].map(lambda x: {
'полный рабочий день': 1,
'частичная занятость': 2,
'удаленная работа': 3,
'сменный график': 4,
'свободный график': 5,
'вахта': 6
}[x])
eda_data.head()
| id | custom_position | schedule | salary_from | salary_to | salary_pay_type | offer_education_id | education_name | education_order_num | city_id | list_regions | work_skills | tags_id | graphic | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 48202096 | Сварщик-сборщик | полный рабочий день | 60000 | 120000 | NaN | 0 | 1 | 0 | 2 | [4] | ['сварочные работы', 'сборка изделий по чертеж... | NaN | 1 |
| 1 | 48202097 | Сварщик-монтажник | полный рабочий день | 60000 | 120000 | NaN | 0 | 1 | 0 | 2 | [4] | ['монтажные работы', 'строительные работы', 'э... | NaN | 1 |
| 2 | 48202098 | Слесарь-сборщик | полный рабочий день | 60000 | 80000 | NaN | 0 | 1 | 0 | 2 | [4] | ['работа на фрезерных станках', 'слесарный рем... | NaN | 1 |
| 3 | 48202356 | Грузчик-упаковщик | частичная занятость | 30000 | 35000 | NaN | 0 | 1 | 0 | 1 | [3] | ['комплектация товара', 'маркировка', 'стрессо... | [6, 9] | 2 |
| 4 | 48202357 | Грузчик-упаковщик | частичная занятость | 30000 | 35000 | NaN | 0 | 1 | 0 | 57 | [181, 182, 183, 185, 186, 187, 188, 189, 190, ... | ['маркировка', 'стрессоустойчивость', 'погрузо... | [6, 9] | 2 |
eda_data.columns
Index(['id', 'custom_position', 'schedule', 'salary_from', 'salary_to',
'salary_pay_type', 'offer_education_id', 'education_name',
'education_order_num', 'city_id', 'list_regions', 'work_skills',
'tags_id', 'graphic'],
dtype='object')
numeric_df = eda_data.select_dtypes(include=['int', 'float'])
correlation_matrix = numeric_df.corr()
plt.figure(figsize=(15, 8))
sns.heatmap(
correlation_matrix,
annot=True,
cmap='coolwarm',
fmt=".2f",
vmin=-1,
vmax=1,
center=0,
linewidths=0.5)
plt.title('Correlation Matrix of Numeric Columns')
plt.xlabel('X')
plt.ylabel('Y')
plt.xticks(rotation=45, ha='right')
plt.show()
report=ProfileReport(df_2)
report
Summarize dataset: 0%| | 0/5 [00:00<?, ?it/s]
Generate report structure: 0%| | 0/1 [00:00<?, ?it/s]
Render HTML: 0%| | 0/1 [00:00<?, ?it/s]
sample_df = df_2.sample(n=1000)
def create_boxplot(data, x_column, y_column, title, figsize=(15, 8)):
"""Creates a boxplot using Seaborn.
Args:
data: The data to use to create the boxplot.
x_column: The name of the x-axis column.
y_column: The name of the y-axis column.
title: The title of the boxplot.
figsize: The figure size of the plot.
"""
plt.figure(figsize=figsize)
sns.boxplot(
x=x_column,
y=y_column,
showmeans=True,
notch=True,
whis=1.5,
data=data,
)
plt.xticks(rotation=-5)
plt.title(title)
plt.show()
# Create the boxplots.
create_boxplot(sample_df, "schedule", "salary_to", "Salary Distribution by Schedule Level")
create_boxplot(sample_df, "schedule", "salary_from", "Salary Distribution by Schedule Level")
create_boxplot(sample_df, "education_name", "salary_to", "Salary Distribution by Education Level")
create_boxplot(sample_df, "education_name", "salary_from", "Salary Distribution by Education Level")
plt.figure(figsize=(15, 8))
sns.boxplot(
x='schedule',
y='salary_from',
showmeans=True,
notch=True,
whis=1.5,
data=sample_df)
plt.xticks(rotation=-25)
plt.title('Salary Distribution by Schedule Level')
plt.show()
def create_scatter_plot(data, x_column, y_column, title, palette):
"""Creates a scatter plot using Seaborn.
Args:
data: The data to use to create the scatter plot.
x_column: The name of the x-axis column.
y_column: The name of the y-axis column.
title: The title of the scatter plot.
palette: The color palette to use for the scatter plot.
"""
sns.scatterplot(
x=x_column,
y=y_column,
alpha=0.5,
palette=palette,
data=data,
)
plt.xlabel(x_column)
plt.ylabel(y_column)
plt.title(title)
plt.show()
# Create the scatter plots.
create_scatter_plot(eda_data, "education_name", "salary_to", "Scatter Plot: Salary vs. Education Level (Salary To)", "viridis")
create_scatter_plot(eda_data, "education_name", "salary_from", "Scatter Plot: Salary vs. Education Level (Salary From)", "inferno")
<ipython-input-105-0a3e98970c34>:12: UserWarning: Ignoring `palette` because no `hue` variable has been assigned. sns.scatterplot(
<ipython-input-105-0a3e98970c34>:12: UserWarning: Ignoring `palette` because no `hue` variable has been assigned. sns.scatterplot(
sns.boxplot(data=df_2, x='schedule', y='salary_from')
plt.xticks(rotation=45)
plt.title('Salary Distribution by Education Level')
plt.show()
sns.boxplot(data=df_2, x='schedule', y='salary_to')
plt.xticks(rotation=45)
plt.title('Salary Distribution by Education Level')
plt.show()
def plot_salary_distribution_by_schedule_top_1000(df):
"""Plots the salary distribution by schedule for the top 1000 rows of the given DataFrame.
Args:
df: A Pandas DataFrame containing the salary and schedule data.
"""
df = df_2.head(1000)
sns.boxplot(
data=df,
x='schedule',
y='salary_from',
showmeans=True
)
plt.xticks(rotation=45)
plt.title('Salary Distribution by Schedule (Top 1000 Rows)')
plt.show()
plot_salary_distribution_by_schedule_top_1000(df)
График показывает, что существует положительная корреляция между уровнем образования и заработной платой. Это означает, что люди с более высоким уровнем образования, скорее всего, будут зарабатывать больше. Однако корреляция не идеальна.
Некоторые люди с более низким уровнем образования зарабатывают больше, чем люди с более высоким уровнем образования, но в целом люди с более высоким уровнем образования зарабатывают больше.
Также стоит отметить, что распределение заработной платы для каждого уровня образования асимметрично вправо, что означает, что больше людей зарабатывают более низкую заработную плату, чем людей, зарабатывающих более высокую заработную плату.
report.to_widgets()
/usr/local/lib/python3.10/dist-packages/ydata_profiling/profile_report.py:507: UserWarning: Ipywidgets is not yet fully supported on Google Colab (https://github.com/googlecolab/colabtools/issues/60).As an alternative, you can use the HTML report. See the documentation for more information. warnings.warn(
VBox(children=(Tab(children=(Tab(children=(GridBox(children=(VBox(children=(GridspecLayout(children=(HTML(valu…
report.to_notebook_iframe()
eda_data['education_is_base'] = eda_data['education_is_base'].astype(int)
eda_data.columns
Index(['id', 'custom_position', 'schedule', 'salary_from', 'salary_to',
'salary_pay_type', 'offer_education_id', 'education_name',
'education_is_base', 'education_order_num', 'city_id', 'list_regions',
'work_skills', 'tags_id', 'graphic'],
dtype='object')
columns_to_pick = ['id', 'custom_position', 'salary_from', 'salary_to', 'offer_education_id', 'education_name',
'education_is_base', 'education_order_num', 'city_id', 'education_level', 'schedule']
eda_data = eda_data.filter(columns_to_pick)
eda_data
| id | custom_position | salary_from | salary_to | offer_education_id | education_name | education_is_base | education_order_num | city_id | schedule | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 48202096 | Сварщик-сборщик | 60000 | 120000 | 0 | 1 | 1 | 0 | 2 | полный рабочий день |
| 1 | 48202097 | Сварщик-монтажник | 60000 | 120000 | 0 | 1 | 1 | 0 | 2 | полный рабочий день |
| 2 | 48202098 | Слесарь-сборщик | 60000 | 80000 | 0 | 1 | 1 | 0 | 2 | полный рабочий день |
| 3 | 48202356 | Грузчик-упаковщик | 30000 | 35000 | 0 | 1 | 1 | 0 | 1 | частичная занятость |
| 4 | 48202357 | Грузчик-упаковщик | 30000 | 35000 | 0 | 1 | 1 | 0 | 57 | частичная занятость |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 19484 | 48737855 | Кладовщик | 45000 | 70000 | 2 | 4 | 1 | 20 | 1 | полный рабочий день |
| 19485 | 48737859 | Кассир | 35000 | 58000 | 0 | 1 | 1 | 0 | 1 | сменный график |
| 19486 | 48737860 | Инженер по медицинской технике | 77000 | 77000 | 4 | 3 | 1 | 10 | 1 | полный рабочий день |
| 19487 | 48737871 | Автомеханик-автослесарь | 80000 | 120000 | 0 | 1 | 1 | 0 | 2 | полный рабочий день |
| 19488 | 48737872 | Автомеханик-автослесарь | 80000 | 120000 | 0 | 1 | 1 | 0 | 102 | полный рабочий день |
19489 rows × 10 columns
В качестве y будем использовать поле salary_from
Спикок кодировок регионов и тэгов - в отдельном справочнике.
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid")
plt.style.use('ggplot')
# DATA_FOLDER = os.path.join('data') # путь к директории с данными
from google.colab import files
uploaded = files.upload()
data = pd.read_csv(os.path.join(DATA_FOLDER, 'data_vacancies.csv'))
data.head(3)
| id | custom_position | schedule | salary_from | salary_to | salary_pay_type | offer_education_id | education_name | education_is_base | education_order_num | city_id | list_regions | work_skills | tags_id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 48202096 | Сварщик-сборщик | полный рабочий день | 60000 | 120000 | NaN | 0 | любое | True | 0 | 2 | [4] | ['сварочные работы', 'сборка изделий по чертеж... | NaN |
| 1 | 48202097 | Сварщик-монтажник | полный рабочий день | 60000 | 120000 | NaN | 0 | любое | True | 0 | 2 | [4] | ['монтажные работы', 'строительные работы', 'э... | NaN |
| 2 | 48202098 | Слесарь-сборщик | полный рабочий день | 60000 | 80000 | NaN | 0 | любое | True | 0 | 2 | [4] | ['работа на фрезерных станках', 'слесарный рем... | NaN |
data.describe(include='all').T['count']
id 19489.0 custom_position 19489 schedule 19489 salary_from 19489.0 salary_to 19489.0 salary_pay_type 106 offer_education_id 19489.0 education_name 19489 education_is_base 19489 education_order_num 19489.0 city_id 19489.0 list_regions 19489 work_skills 19489 tags_id 13490 Name: count, dtype: object
print(f"Общее кол-во вакансий - {len(data):,}.")
Общее кол-во вакансий - 19,489.
data['salary_from'].describe()
count 19489.000000 mean 58869.138848 std 30248.195246 min 20500.000000 25% 40000.000000 50% 50000.000000 75% 70000.000000 max 750000.000000 Name: salary_from, dtype: float64
top20_profs = data['custom_position'].value_counts(dropna=False).nlargest(20)
top20_profs
Продавец-кассир 409 Менеджер по продажам 290 Продавец-консультант 238 Курьер 193 Охранник 134 Повар 130 Разнорабочий 127 Водитель по доставке документов 118 Грузчик 118 Комплектовщик 112 Работник торгового зала 105 Продавец 96 Кладовщик 96 Менеджер по работе с клиентами 95 Оператор call-центра / Менеджер по работе с клиентами (удаленно) 95 Оператор call-центра 94 Мерчандайзер-грузчик 94 Оператор входящих звонков 87 Водитель-экспедитор 87 Оператор call-центра (удаленно) 78 Name: custom_position, dtype: int64
data[data['city_id'] == 1]['custom_position'].value_counts(dropna=False).nlargest(20)
Менеджер по продажам 179 Продавец-кассир 166 Курьер 123 Продавец-консультант 115 Повар 86 Оператор входящих звонков 80 Копирайтер 72 Контент-менеджер 65 Менеджер социальных сетей 58 Помощник копирайтера 55 Работник торгового зала 55 Мерчандайзер-грузчик 54 Оператор call-центра 52 Грузчик 52 Охранник 50 Уборщик/ца 49 Помощник разработчика сайтов 49 Кладовщик 48 Помощник контент-менеджера 47 Менеджер по работе с клиентами магазина бытовой техники и электроники (Входящие звонки) 47 Name: custom_position, dtype: int64
data[data['city_id'] == 2]['custom_position'].value_counts(dropna=False).nlargest(20)
Продавец-кассир 67 Менеджер по продажам 50 Охранник 44 Продавец-консультант 44 Курьер 42 Домработница приходящая 35 Уборщик, Уборщица 28 Разнорабочий 26 Кладовщик 26 Продавец 21 Уборщик/Уборщица 21 Работник торгового зала 19 Грузчик 18 Менеджер по работе с клиентами 18 Бариста 17 Сборщик интернет-заказов 17 Администратор 15 Электромонтажник 15 Заместитель директора магазина 15 Автомеханик-автослесарь 15 Name: custom_position, dtype: int64
_df = data[data['custom_position'].isin(top20_profs.index)]
profession_ranking = list(top20_profs.index)
f, ax = plt.subplots(figsize=(12,5))
ax = sns.boxenplot(x="custom_position", y="salary_from",
color="gray", palette="Set3", order=profession_ranking,
scale="linear", data=_df, linewidth=0.5)
ax.tick_params(axis='x', rotation=90)
ax.set_title("Зарплата по ТОП 20 професиям", fontsize=14)
means = _df.groupby("custom_position")["salary_from"].mean().loc[profession_ranking]
_ = plt.plot(range(len(profession_ranking)), means, marker="o", color="green", markersize=6, linestyle="--")
from sklearn.model_selection import train_test_split
data_train, data_test, y_train, y_test = train_test_split(data[['custom_position']],
data[['salary_from']],
test_size=0.2,
random_state=42)
data_train.shape, data_test.shape
((15591, 1), (3898, 1))
import fasttext
import fasttext.util
import numpy as np
from scipy import spatial
ft = fasttext.load_model('../cc.ru.300.bin')
Warning : `load_model` does not return WordVectorModel or SupervisedModel any more, but a `FastText` object which is very similar.
fasttext.util.reduce_model(ft, 100) # уменьшаем размерность вектора
<fasttext.FastText._FastText at 0x7fa00ae37850>
ft.get_dimension()
100
ft.get_word_vector('водитель')[:10]
array([-0.02232989, -0.02558348, 0.0685254 , 0.04912743, 0.01441588,
0.01026478, -0.10471214, 0.03264587, 0.05108723, 0.11439214],
dtype=float32)
def get_cosine_similarity(a: list, b: list):
return 1 - spatial.distance.cosine(a, b)
def get_vector(s, model=ft):
s = s.lower().strip()
return model.get_word_vector(s)
get_vector('Водитель ')[:10]
array([-0.02232989, -0.02558348, 0.0685254 , 0.04912743, 0.01441588,
0.01026478, -0.10471214, 0.03264587, 0.05108723, 0.11439214],
dtype=float32)
get_cosine_similarity(get_vector('водитель'), get_vector('таксист'))
0.8104820251464844
get_cosine_similarity(get_vector('тракторист'), get_vector('фотомодель'))
0.3793732523918152
from sklearn.preprocessing import FunctionTransformer
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.neighbors import KNeighborsRegressor
from sklearn.model_selection import KFold, GridSearchCV
from sklearn.metrics import r2_score, mean_absolute_percentage_error
class DataFrameSelector(BaseEstimator, TransformerMixin):
def __init__(self, attribute_names):
self.attribute_names = attribute_names
def fit(self, X, y=None):
return self
def transform(self, X):
return X[self.attribute_names]
class ProcessingTextFeatures(BaseEstimator, TransformerMixin):
def __init__(self):
pass
def fit(self, X, y=None):
return self
@staticmethod
def text_processing(s):
if s:
return s.lower().strip()
else:
return None
def transform(self, X):
for a in X.columns:
X[a] = X[a].apply(lambda x: self.text_processing(x))
return X
class TextEmbeddings(BaseEstimator, TransformerMixin):
def __init__(self, lang_model):
self.lang_model = lang_model
def fit(self, X, y=None):
return self
@staticmethod
def get_features_from_vector(vectors):
df = pd.DataFrame()
for v in vectors:
df = pd.concat([df, pd.DataFrame(v).T])
_cols = [f"feature_{c}" for c in df.columns]
df.columns = _cols
return df
def transform(self, X):
X['vector_professions'] = X.iloc[:,0].apply(
lambda words: np.mean([get_vector(word) for word in words.split(" ")], axis=0))
return self.get_features_from_vector(X['vector_professions'])
profession_embedding_pipeline = Pipeline([
("select_features", DataFrameSelector(attribute_names=["custom_position"])),
("processing_text_features", ProcessingTextFeatures()),
("get_profession_embeddings", TextEmbeddings(lang_model=ft)),
])
X_train = profession_embedding_pipeline.fit_transform(data_train)
X_train.head(3)
| feature_0 | feature_1 | feature_2 | feature_3 | feature_4 | feature_5 | feature_6 | feature_7 | feature_8 | feature_9 | ... | feature_90 | feature_91 | feature_92 | feature_93 | feature_94 | feature_95 | feature_96 | feature_97 | feature_98 | feature_99 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.015912 | 0.001667 | 0.024425 | -0.033466 | 0.028346 | 0.002949 | -0.009210 | 0.010261 | 0.012796 | 0.009221 | ... | 0.002755 | -0.005422 | 0.022568 | -0.017908 | 0.009551 | 0.033006 | -0.006285 | 0.021489 | -0.007438 | -0.028621 |
| 0 | 0.012033 | 0.064572 | 0.030388 | -0.082866 | -0.061539 | 0.052734 | -0.057564 | 0.055990 | -0.013068 | -0.059835 | ... | -0.006975 | 0.030193 | -0.012232 | -0.028487 | -0.031524 | -0.013782 | 0.049508 | 0.043902 | 0.024200 | 0.045752 |
| 0 | -0.002965 | 0.054473 | 0.010346 | -0.056326 | -0.138993 | 0.012678 | 0.025266 | 0.074584 | 0.061354 | -0.024473 | ... | 0.015798 | 0.037486 | 0.035533 | -0.017324 | -0.044231 | -0.040195 | 0.052769 | 0.008908 | 0.012552 | 0.083026 |
3 rows × 100 columns
num_folds = 5
seed = 7
scoring = 'neg_mean_absolute_error'
neighbors = [1, 3, 5, 7, 9, 15]
param_grid = dict(n_neighbors=neighbors)
model = KNeighborsRegressor()
kfold = KFold(n_splits=num_folds)
grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring=scoring, cv=kfold)
grid_result = grid.fit(X_train, y_train)
print("Best: %f using %s" % (-1 * grid_result.best_score_, grid_result.best_params_))
means = grid_result.cv_results_['mean_test_score']
stds = grid_result.cv_results_['std_test_score']
params = grid_result.cv_results_['params']
for mean, stdev, param in zip(means, stds, params):
print("MAE: %f (%f) with: %r" % (-1 * mean, stdev, param))
Best: 13900.328053 using {'n_neighbors': 5}
MAE: 15016.689200 (399.050735) with: {'n_neighbors': 1}
MAE: 14012.989705 (346.416102) with: {'n_neighbors': 3}
MAE: 13900.328053 (361.543882) with: {'n_neighbors': 5}
MAE: 14064.296092 (295.366199) with: {'n_neighbors': 7}
MAE: 14186.445977 (260.040995) with: {'n_neighbors': 9}
MAE: 14561.963819 (224.822423) with: {'n_neighbors': 15}
#evaluation - baselines
num_folds = 5
seed = 7
scoring = 'neg_mean_absolute_error'
models = []
models.append(('LR', LinearRegression()))
models.append(('RidgeRegression', Ridge()))
models.append(('LassoRegression', Lasso()))
models.append(('KNNRegression', KNeighborsRegressor()))
models.append(('DecisionTreeRegressor', DecisionTreeRegressor()))
results = []
names = []
for name, model in models:
kfold = KFold(n_splits=num_folds, random_state=seed, shuffle=True)
# умножаем на (-1) из-за особеностей работы cross_val_score
cv_results = -1 * cross_val_score(model, X_train, y_train, cv=kfold, scoring=scoring)
results.append(cv_results)
names.append(name)
msg = "%s %f %f " % (name, cv_results.mean(), cv_results.std())
print(msg)
best_knn = grid.best_estimator_
KNeighborsRegressor()
preds = best_knn.predict(profession_embedding_pipeline.fit_transform(data_test))
mape = mean_absolute_percentage_error(y_test, preds)
print(f"mape = {round(mape, 3)}")
mape = 0.227